package org.jugile.util;

/*

Copyright (C) 2007-2011 Jukka Rahkonen  email: jukka.rahkonen@iki.fi

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/

/**
 * ""
 * BUG: data/text node handling doesn't work 
 * @author jukka.rahkonen@iki.fi
 */
public class XmlParser extends Jugile {

	// ----- parser -----
	
	// There are five predefined entities: &lt; represents "<", &gt; represents ">", 
	// &amp; represents "&", &apos; represents ', and &quot; represents ". All
	// permitted Unicode characters may be represented with a numeric character 
	// reference. Consider the Chinese character "中", whose numeric code in Unicode
	// is hexadecimal 4E2D, or decimal 20,013. A user whose keyboard offers no method 
	// for entering this character could still insert it in an XML document encoded 
	// either as &#20013; or &#x4e2d;

	public static Node parse(PullReader pr) {
		XmlParser p = new XmlParser();
		p.doParse(pr);
		pr.close();
		return p.n;
	}
	
	private enum State { 
		START, TAG, ENDTAG, ATTRNAME, ATTRVALUE, COMMENT, XMLHEADER, CDATA 
	};

	private Node n;
	
	private void doParse(PullReader pr) {
		Buffer buf = new Buffer();
		State state = State.START;
		pr.trim();
		while (pr.hasNext()) {
			char ch = pr.next();
			if (state == State.START) {
				//print("start: " + ch);
				if (ch == '<') {
					if (pr.peek() == '?') {
						pr.skip(1);
						state = State.XMLHEADER;
						continue;
					}
					if (pr.peek() == '!') {
						String str = pr.str(3);
						if ("!--".equals(str)) {
							state = State.COMMENT;
							continue;
						}
						str = str + pr.str(5);
						if ("![CDATA[".equals(str)) {
							state = State.CDATA;
							continue;
						}
						fail("parse error: <!" + pr.getPositionString());
					}
					if (pr.peek() == '/') {
						// end tag
						String endtag = pr.readUntil(">");
						pr.skip(1);
						//print("endtag: " + endtag);
						if (n.parent() != null) n = n.parent();
						continue;
					}
					//print("state-> TAG");
					state = State.TAG;
					continue;					
				}
				// text node until next "<"
				String data = pr.readUntil("<");
				if (!empty(data)) {
					//print("textnode: " + ch + data);
					n.add("", ch+data);
				}
				continue;
			}
			if (state == State.TAG) {
				pr.push(ch);
				//pr.trim();
				String name = parseName(pr,"/>");
				//print("name: " + name);
				if (pr.peek(1) == '>') {
					pr.skip(1);
					state = State.START;
					if (n == null) n = new Node(name);
					else n = n.add(name);
					continue;
				}
				if (pr.peek(1) == '/' && pr.peek(2) == '>') {
					pr.skip(2);
					state = State.START;
					if (n == null) n = new Node(name);
					else n.add(name);
					continue;					
				}
				// read attributes
				if (n == null) n = new Node(name);
				else n = n.add(name);
				while (parseAttribute(pr));
				state = State.START;
				continue;
			}
			if (state == State.XMLHEADER) {
				if (ch == '?') {
					if (pr.peek() == '>') {
						state = State.START;
						pr.skip(1);
						//print("xmlheader: " + buf.toString());
						buf = new Buffer();
						continue;
					}
				}
				buf.add(ch);
				continue;
			}
			if (state == State.COMMENT) {
				if (ch == '-') {
					if (pr.peek() == '-' && pr.peek(2) == '>') {
						state = State.START;
						pr.skip(2);
						//print("comment: " + buf.toString());
						buf = new Buffer();
						continue;
					}
				}
				buf.add(ch);
				continue;
			}
			if (state == State.CDATA) {
				if (ch == ']') {
					if (pr.peek() == ']' && pr.peek(2) == '>') {
						state = State.START;
						pr.skip(2);
						if (n != null) n.addCdata(buf.toString());
						//print("cdata: " + buf.toString());
						buf = new Buffer();
						continue;
					}
				}
				buf.add(ch);
				continue;
			}
		}
	}


	private boolean parseAttribute(PullReader pr) {
		pr.trim();
		String aname = parseName(pr,"=/>");
		if (empty(aname)) return false;
		pr.trim();
		if (pr.peek() == '=') {
			String value = parseValue(pr);
			//print("  attr: " + aname + " = " + value);
			n.attr(aname, value);
			return true;
		}
		n.attr(aname,"");
		return true;
	}
	
	private String parseName(PullReader pr, String stopchars) {
		Buffer buf = new Buffer();
		pr.trim();
		while (pr.hasNext()) {
			char ch = pr.next();
			//print("namech: " + ch);
			if (Character.isWhitespace(ch)) return buf.toString();
			if (stopchars.indexOf(ch) >= 0) {
				pr.push(ch);
				return buf.toString();
			}
			buf.add(ch);
		}
		return buf.toString();
	}
	
	private String parseValue(PullReader pr) {
		pr.trim();
		char ch = pr.next();
		char quot = 0;
		if (ch == '/' || ch == '>') fail("parse error: " + pr.getPositionString());
		Buffer buf = new Buffer();
		if (ch == '"' || ch == '\'') quot = ch;
		while (pr.hasNext()) {
			ch = pr.next();
			if (quot == 0) {
				if (Character.isWhitespace(ch)) return buf.toString();
				if (ch == '/' || ch == '>') { pr.push(ch); return buf.toString(); }
			} else {
				if (ch == quot) return buf.toString();
			}
			buf.add(ch);
		}
		return buf.toString();
	}
	
	
	private char parseEntity(PullReader pr) {
		Buffer buf = new Buffer();
		while (pr.hasNext()) {
			char ch = pr.next();
			if (ch == ';') {
				String ent = buf.toString();
				return 0;
			}
		}
		return 0;
	}

}
